import { Device, DeviceConfig, DeviceRotation, STFEaseConfig } from './interfaces'
import { exec } from 'child_process'
import * as path from 'path'
import { check } from 'tcp-port-used'
import * as fs from 'fs'
import * as gm from 'gm'
import { logger } from 'f2e-server3'

export { logger }

export const createImageCrop = (device: Device) => {
    const size = device.size.override || device.size.physical || ''
    let [originWidth, originHeight] = size.match(/\d+/g)?.map(Number) || []
    let h = device.runtime_size?.match(/\d+/g)?.map(Number).pop() || 0
    let w = Math.floor(h * originWidth / originHeight)
    let [x, y] = [0, 0]

    switch (device.rotation) {
        case DeviceRotation.R90:
        case DeviceRotation.R270:
            let tmp = w; w = h; h = tmp;
            break;
    }

    return (img: Buffer) => new Promise<Buffer>(function (resolve, reject) {
        gm(img).crop(w, h, x, y).toBuffer('jpeg', function (err, buffer) {
            if (err) {
                reject(err)
            }
            else {   
                resolve(buffer)
            }
        })
    })
}

export enum DebugLevel {
    INFO = 'info',
    ERROR = 'error',
    NULL = 'null'
}
export const DEBUG_LEVEL: DebugLevel = process.env.DEBUG_LEVEL as DebugLevel || DebugLevel.ERROR
export const delay = (timeout: number) => new Promise(resolve => setTimeout(resolve, timeout))

const used_ports_set = new Set<number>()
/**
 * 设置使用端口
 * @param ports 
 * @returns 
 */
export const usePorts = (...ports: number[]) => ports.forEach(p => used_ports_set.add(p))
/**
 * 释放已经使用端口
 * @param ports 
 * @returns 
 */
export const releasePorts = (...ports: number[]) => ports.forEach(p => used_ports_set.delete(p))
/**
 * 获取可用端口 不能通过尝试tcp信息判断，所以不能使用 tcp-port-used
 * @param beginPort 起始端口
 * @param n 共需要获取多少个端口
 * @returns 所有可用端口
 */
export const getIdlePorts = async (beginPort: number, n = 1) => {
    let port = beginPort
    let arr: number[] = []
    for (let i = 0; i < n; i++) {
        let used = used_ports_set.has(port) || await check(port)
        while (used) {
            port++
            used = used_ports_set.has(port) || await check(port)
        }
        arr.push(port)
        port++
    }
    return arr
}

/**
 * 有些命令执行可能卡死， 给一个超时时间自动释放
 * @param cmd 要执行的命令
 * @param interval 超时时间
 * @returns 
 */
export const execSafe = (cmd: string, interval = 5000) => new Promise<string>((resolve, reject) => {
    let timer: NodeJS.Timeout
    // logger.debug('Exec: ', cmd)
    let proc = exec(cmd, function (err, out) {
        if (err) {
            resolve('')
        } else {
            resolve(out)
        }
        clearTimeout(timer)
    })
    timer = setTimeout(function () {
        if (!proc.killed) {
            logger.error('Exec Timeout: ', cmd)
            proc.kill(0)
        }
    }, interval)
})




/**
 * 设备信息缓存
 * 针对设备特点需要设置不同的分辨率频率等参数以期达到最佳效果，因此需要进行设备信息配置缓存
 */
export class DeviceManager {
    config_map: Record<string, DeviceConfig>
    device_map: Record<string, Device>
    /**
     * @param db_devices 设备信息存储位置, 每天获取一次，避免频繁的shell调用
    */
    constructor () {
        this.device_map = {}
        this.config_map = {}

        const DEVICE_CONFIG = path.join(__dirname, '../device_config.json')
        if (fs.existsSync(DEVICE_CONFIG)) {
            try {
                this.config_map = JSON.parse(fs.readFileSync(DEVICE_CONFIG).toString())
            } catch (e) {
                logger.error('device_config.json is not a json file')
            }
        }
    }
    private __get_create_date = () => {
        const d = new Date()
        return `${d.getFullYear()}/${d.getMonth() + 1}/${d.getDate()}`
    }
    saveDevice = (device: Device) => {
        device.create_date = this.__get_create_date()
        this.device_map[device.id] = device
        return device
    }
    getDevice = (id: string) => {
        let device = this.device_map[id]
        if (device && device.create_date === this.__get_create_date()) {
            return device
        } else {
            return null
        }
    }
    getDeviceConfig = (id: string) => {
        return this.config_map[id] || {}
    }
}

/**
 * 支持的属性获取
 */
export enum PropType {
    ABI = 'ro.product.cpu.abi',
    SDK = 'ro.build.version.sdk',
    BRAND = 'ro.product.brand',
    MODEL = 'ro.product.model',
    BOARD = 'ro.product.board',
}
export interface ApkInfo {
    filename: string,
    pkgname: string
}
export enum ApkType {
    STFService,
    Yosemite,
}
export const ApkTypeMap: Record<ApkType, ApkInfo> = {
    [ApkType.STFService]: {
        filename: path.join(__dirname, '../apks/STFService.apk'),
        pkgname: 'jp.co.cyberagent.stf',
    },
    [ApkType.Yosemite]: {
        filename: path.join(__dirname, '../apks/Yosemite.apk'),
        pkgname: 'com.netease.nie.yosemite',
    }
}
/**
 * adb命令封装
 */
export class ADBFactory {
    static device_map = new Map<string, Device>()
    target = '/data/local/tmp'
    adb: string
    /**
     * @param adb adb命令
    */
    constructor (adb: string) {
        this.adb = adb
    }
    /**
     * 清除 adb forward
     */
    clearForward = async () => {
        const res = await execSafe(`${this.adb} forward --list | grep 'localabstract:'`)
        const locals = res.match(/[^\r\n]+/g) || []
        for(let i = 0; i < locals.length; i++) {
            const [ id, tcp ] = locals[i].split(/[\s\t]+/)
            await execSafe(`${this.adb} -s ${id} forward --remove ${tcp}`)
        }
    }
    /**
     * 删除指定设备adb任务
     * @param id 设备ID
     */
    clearAdbTask = async (id: string) => {
        const res = await execSafe(`${this.adb} forward --list | grep 'localabstract:' | grep ${id}`)
        const locals = res.match(/[^\r\n]+/g) || []
        for(let i = 0; i < locals.length; i++) {
            const [ id, tcp ] = locals[i].split(/[\s\t]+/)
            await execSafe(`${this.adb} -s ${id} forward --remove ${tcp}`)
        }
        return new Promise<void>((resolve) => {
            exec(`ps -ef | grep "${this.adb} -s ${id} shell" | awk -F " " '{print $2F}' | xargs kill -9`, function () {
                setTimeout(resolve, 1000)
            })
        })
        // await execSafe(`${this.adb} -s ${id} shell 'ps -ef | pgrep "minicap" | xargs kill -9'`)
        // await execSafe(`${this.adb} -s ${id} shell 'ps -ef | pgrep "minitouch" | xargs kill -9'`)
        // await execSafe(`${this.adb} -s ${id} shell 'ps -ef | pgrep "stf.agent" | xargs kill -9'`)
    }
    /**
     * 获取所有设备id
    */
    devices = async () => {
        let res = await execSafe(`${this.adb} devices`)
        const devices: Device[] = []
        const items = (res.match(/[^\r\n]+/g) || ['']).slice(1)
        for (let i = 0; i < items.length; i++) {
            const line = items[i];
            const [id, status] = line.split(/[\s\t]+/)
            const device = await this.getDevice(id)
            if (device) {
                devices.push({
                    ...device,
                    status: status === 'device' ? 'device' : 'offline',
                })
            }
        }
        return devices
    }
    getProp = async (id: string, prop: PropType) => {
        const res = await execSafe(`${this.adb} -s ${id} shell getprop ${prop}`)
        return res.trim()
    }
    getRotation = async (id: string): Promise<DeviceRotation> => {
        const res = await execSafe(`${this.adb} -s ${id} shell dumpsys display | egrep "mOverrideDisplayInfo.*rotation [0-3]"`, 1000)
        if (res) {
            const [, rotation] = res.match(/mOverrideDisplayInfo.*?rotation\s([0-3])/) || []
            return Number(rotation)
        }
        return 0
    }
    shell = async (id: string, cmd: string) => {
        return execSafe(`${this.adb} -s ${id} shell ${cmd}`)
    }
    /**
     * 通过adb获取设备基本信息
     * @param id 设备id
     * @returns 
     */
    getDevice = async (id: string) => {
        const _d = ADBFactory.device_map.get(id)
        if (_d) {
            return _d
        }
        const abi = await this.getProp(id, PropType.ABI)
        const sdk = await this.getProp(id, PropType.SDK)
        const brand = await this.getProp(id, PropType.BRAND)
        const model = await this.getProp(id, PropType.MODEL)
        const board = await this.getProp(id, PropType.BOARD)
        const rotation = await this.getRotation(id) || 0
        const [ physical, override ] = (await execSafe(`${this.adb} -s ${id} shell wm size`))
            .match(/[^\r\n]+/g)?.map(line => {
                const [a, b, c] = line.match(/(\d+)x(\d+)/) || []
                return a
            }) || []
        const device: Device = {
            id, abi, sdk: Number(sdk), brand, model, board,
                size: { physical, override }, rotation,
        }
        ADBFactory.device_map.set(id, device)
        return device
    }
    /**
     * 上传文件并修改权限
     * @param id 设备ID
     * @param filename 完整文件路径
     * @returns 设置完成后的位置
     */
    pushFile = async (id: string, filename: string) => {
        const _filename = filename.split(/[\\\/]/).pop()
        await execSafe(`${this.adb} -s ${id} push ${filename} ${this.target}`)
        await delay(2000)
        await execSafe(`${this.adb} -s ${id} shell "chmod 777 ${this.target}/${_filename}"`)
        return `${this.target}/${_filename}`
    }

    /**
     * 检查并安装APK
     * @param id 设备ID
     * @param type apk包类型
     */
    installApk = (id: string, type: ApkType | string) => {
        const apk = ApkTypeMap[type]
        const t = this
        if (apk) {
            return new Promise<void>(resolve => exec(`${this.adb} -s ${id} shell "pm list packages | grep '${apk.pkgname}'"`, function (err, out) {
                let installed = err ? false : out
                if (!installed) {
                    /** 安装apk可以等待1分钟 */
                    execSafe(`${t.adb} -s ${id} install -r ${apk.filename}`, 60 * 1000)
                    .finally(resolve)
                } else {
                    resolve()
                }
            }))
        } else {
            return execSafe(`${t.adb} -s ${id} install -r ${type}`, 60 * 1000)
        }
    }

    /**
     * 
     * @param device 
     * @param size minicap size
     * @returns 成功true，失败false
     */
    start_minicap = async (device: Device, config: STFEaseConfig) => {
        // return Promise.resolve(false)
        let rate = ` -r ${config.rate}`
        let rotate = [0, 90, 180, 270][device.rotation || 0] || 0
        const sh_minicap = `${this.adb} -s ${device.id} shell LD_LIBRARY_PATH=${this.target} ${this.target}/minicap${rate} -P ${device.runtime_size}/${rotate}`
        let is_run = true
        // clear minicap
        await execSafe(`ps -ef | grep "${this.adb} -s ${device.id} shell LD_LIBRARY_PATH=${this.target} ${this.target}/minicap" | awk -F " " '{print $2F}' | xargs kill -9`);
        // await execSafe(`${this.adb} -s ${device.id} shell 'ps -ef | pgrep "minicap" | xargs kill -9'`)
        logger.info('start minicap: \t', sh_minicap)
        exec(sh_minicap, function (err, out) {
            let outstr = err ? err.toString() : out
            if (outstr.includes('Aborted')) {
                logger.info('start minicap Abort! \t')
                is_run = false
            } else if (outstr.includes('CANNOT LINK EXECUTABLE')) {
                logger.info('start minicap CANNOT LINK EXECUTABLE! \t')
                is_run = false
            }
        })
        
        return new Promise<boolean>((resolve) => setTimeout(function () {
            resolve(is_run)
        }, 2000))
    }

    /**
     * 
     * @param device 
     * @param scale
     */
    start_javacap = async (device: Device, scale: number, onRelease?: Function) => {
        const apk = ApkTypeMap[ApkType.Yosemite]
        const apk_shell = `${this.adb} -s ${device.id} shell pm path ${apk.pkgname} | tr -d '\\r' | awk -F: '{print $2}'`
        const APK = (await execSafe(apk_shell)).trim()
        let success = true
        if (!APK) {
            logger.error('javacap 启动失败！')
            return false
        }
        const sh_start_javacap = `${this.adb} -s ${device.id} shell CLASSPATH=${APK} exec app_process /system/bin ${apk.pkgname}.Capture --scale ${scale} --socket javacap -lazy 2>&1`
        logger.info('start javacap: \t', sh_start_javacap)
        exec(sh_start_javacap, function (err, out) {
            logger.info('release: ', sh_start_javacap)
            onRelease && onRelease()
            success = false
            return;
            let message = (err || out).toString()
            if (message.includes(`Address already in use`)) {
                logger.info('javacap is started: ', sh_start_javacap)
            } else {
                logger.error(message)
                // onRelease && onRelease()
                // success = false
            }
        })
        await delay(2000)
        return success
    }

    start_minitouch = async (device: Device) => {
        // return false
        const t = this
        const sh_minitouch = `${this.adb} -s ${device.id} shell ${this.target}/minitouch`
        const apk = ApkTypeMap[ApkType.STFService]
        return new Promise<boolean>((resolve, reject) => {
            let running = true
            if (device.sdk > 28) {
                const sh_start_service = `${this.adb} -s ${device.id} shell am start-foreground-service --user 0 -a ${apk.pkgname}.ACTION_START -n ${apk.pkgname}/.Service`
                logger.info('start service: \t', sh_start_service)
                exec(sh_start_service, async function (err) {
                    if (err) {
                        logger.error(err)
                        resolve(false)
                        return
                    }
                    const APK = (await execSafe(`${t.adb} -s ${device.id} shell "pm path ${apk.pkgname} | tr -d '\\r' | awk -F: '{print $2}'"`)).trim()
                    if (!APK || !APK.includes(apk.pkgname)) {
                        logger.error('no apk:', APK, `${t.adb} -s ${device.id} shell "pm path ${apk.pkgname} | tr -d '\\r' | awk -F: '{print $2}'"`)
                        resolve(false)
                        return
                    }
                    const sh_start_agent = `${t.adb} -s ${device.id} shell export CLASSPATH="${APK}"\\\; exec app_process /system/bin ${apk.pkgname}.Agent 2>&1`
                    logger.info('start agent: \t', sh_start_agent)
                    exec(sh_start_agent, function (err, out) {
                        if (err) {
                            logger.error(err)
                            running = false
                        } else {
                            logger.info(out)
                        }
                    })
                    setTimeout(function () {
                        logger.info('start touch: \t', sh_minitouch)
                        if (!running) {
                            resolve(false)
                            return
                        }
                        exec(sh_minitouch, function (err, out) {
                            if (err) {
                                logger.error('stop touch: ', device.id, err)
                                running = false
                            }
                        })
                        setTimeout(function () {
                            resolve(running)
                        }, 1000)
                    }, 1500)
                })
            } else {
                logger.info('start touch: \t', sh_minitouch)
                exec(sh_minitouch, function (err, out) {
                    if (err) {
                        logger.error('stop touch: ', device.id, err)
                        running = false
                    }
                })
                setTimeout(function () {
                    resolve(running)
                }, 2000)
            }
        })
    }
}